Dokploy (Production)

Deploy Makeugcads to your own VPS using Dokploy and GHCR images

Dokploy is a self-hosted PaaS that manages Docker Compose services on your VPS with automatic HTTPS, rolling deployments, and a web UI for environment variables. The production path for this repository is: GitHub Actions builds the app images, migration image, and RBAC init image, pushes them to GitHub Container Registry (GHCR), and Dokploy pulls those prebuilt images.

Deployment Model

This repository's production flow is:

  1. Push code to main or dev
  2. GitHub Actions builds four images:
    • ghcr.io/chenhaonan-eth/ugcad-web:<tag>
    • ghcr.io/chenhaonan-eth/ugcad-agents:<tag>
    • ghcr.io/chenhaonan-eth/ugcad-migrations:<tag>
    • ghcr.io/chenhaonan-eth/ugcad-rbac-init:<tag>
  3. Dokploy uses compose.yml to pull the images, connect to Dokploy Databases PostgreSQL, run one-off migrations / rbac-init, and start redis, agents, and web

Pull requests only run build validation and do not push images. Production Dokploy deployments should pull prebuilt GHCR images, not build source code on the server.

Prerequisites

  • A VPS with at least 4 vCPUs, 8 GB RAM, 80 GB disk (Ubuntu 24.04 recommended)
  • A domain name with DNS A record pointing to your server IP
  • An LLM API key (OpenAI or compatible)
  • GHCR access for the repository images
  • GitHub repository variables configured for public build-time values
  • A PostgreSQL instance prepared in Dokploy Databases

Step 1 — Install Dokploy

SSH into your server and run:

curl -sSL https://dokploy.com/install.sh | sh

Once complete, access the Dokploy UI at http://YOUR_SERVER_IP:3000 and create your admin account.

Step 2 — Configure GitHub Actions Build Variables

The Next.js client bundle in the web image bakes public variables at build time. Configure these in GitHub repository Settings → Secrets and variables → Actions → Variables before building production images:

NEXT_PUBLIC_APP_URL=https://yourdomain.com
NEXT_PUBLIC_APP_NAME=Makeugcads
NEXT_PUBLIC_THEME=huxibo
NEXT_PUBLIC_APPEARANCE=system
NEXT_PUBLIC_DEFAULT_LOCALE=en

NEXT_PUBLIC_APP_URL must be injected as a GitHub repository variable at build time. Setting it only in Dokploy at runtime is not enough for client-visible URLs. If the domain changes, rebuild and publish a new web image.

Secret history audit is a production launch blocker. Before serving production traffic, verify that real .env files, API keys, database passwords, and AUTH_SECRET values were not committed to Git history, exposed in Actions logs, or baked into image layers.

Step 3 — Prepare the Compose File

The repository root compose.yml is production-oriented and pulls four GHCR images:

migrations:
  image: ${GHCR_IMAGE_PREFIX:-ghcr.io/chenhaonan-eth/ugcad}-migrations:${IMAGE_TAG:-latest}

rbac-init:
  image: ${GHCR_IMAGE_PREFIX:-ghcr.io/chenhaonan-eth/ugcad}-rbac-init:${IMAGE_TAG:-latest}

agents:
  image: ${GHCR_IMAGE_PREFIX:-ghcr.io/chenhaonan-eth/ugcad}-agents:${IMAGE_TAG:-latest}

web:
  image: ${GHCR_IMAGE_PREFIX:-ghcr.io/chenhaonan-eth/ugcad}-web:${IMAGE_TAG:-latest}

To deploy a fork or pin a version, set these Dokploy environment variables:

GHCR_IMAGE_PREFIX=ghcr.io/chenhaonan-eth/ugcad
IMAGE_TAG=latest

IMAGE_TAG can also be a branch tag or SHA tag generated by GitHub Actions for canary deploys and rollbacks. Use the same tag for web, agents, migrations, and rbac-init in one deployment.

Step 4 — Create a Project in Dokploy

  1. In the Dokploy UI, click Create Project
  2. Give it a name (e.g., makeugcads)
  3. Inside the project, click Create Service → Docker Compose
  4. Connect the GitHub repository so Dokploy can read compose.yml, or ensure the server deployment directory contains the full repository context
  5. Confirm the deployment mode pulls images instead of building source code on the server

Production PostgreSQL is managed by Dokploy Databases and injected into Compose services through standard PostgreSQL URLs. The default production deploy does not enable the embedded postgres Compose service; that service remains behind the legacy-postgres profile for short rollback windows or local troubleshooting.

This production path does not introduce Supabase Auth, Supabase Storage, Supabase Realtime, self-hosted Supabase, or self-hosted Neon. The database cutover uses Dokploy Databases PostgreSQL and standard PostgreSQL URLs only.

Step 5 — Configure Runtime Environment Variables

Create one Dokploy PostgreSQL service, then create two logical databases in it: one for business data and one for LangGraph runtime state. Copy the internal PostgreSQL URLs from Dokploy when the app and database live in the same Dokploy project/network; keep any sslmode query parameter Dokploy provides, and URL-encode special characters in usernames or passwords.

  • DATABASE_URL points to the business logical database used by migrations, rbac-init, and web. It stores users, auth, RBAC, brand workspace, and UGC business tables.
  • LANGGRAPH_DATABASE_URL points to the LangGraph runtime logical database used by agents for checkpoint/state/store data. Do not mix it with business tables.

In the Dokploy service settings, add the following environment variables. Store real credentials only in Dokploy environment variables/secrets; do not commit them to .env, .env.production, Compose overrides, MDX docs, GitHub Actions logs, or container image layers:

# ── Image selection ───────────────────────────────────
GHCR_IMAGE_PREFIX=ghcr.io/chenhaonan-eth/ugcad
IMAGE_TAG=latest

# ── Infrastructure ────────────────────────────────────
DATABASE_URL=postgresql://<user>:<password>@<host>:5432/<business-db>
LANGGRAPH_DATABASE_URL=postgresql://<user>:<password>@<host>:5432/<langgraph-db>
REDIS_PASSWORD=<strong random password>

# ── Web runtime ───────────────────────────────────────
AUTH_SECRET=<openssl rand -base64 32>
NEXT_PUBLIC_APP_URL=https://yourdomain.com

# ── Inter-service auth ────────────────────────────────
AGENTS_SHARED_SECRET=<random string>
BRAND_WORKSPACE_ACTIVE_BRAND_ID=brand-live

# ── LLM ───────────────────────────────────────────────
OPENAI_API_KEY=sk-your-key
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_MODEL=gpt-4o

# ── Optional ──────────────────────────────────────────
RBAC_ADMIN_EMAIL=
ANTHROPIC_API_KEY=
TAVILY_API_KEY=
MINERU_API_KEY=
TIKHUB_API_TOKEN=
LANGSMITH_API_KEY=
LANGSMITH_TRACING=false
LANGSMITH_TRACING_V2=false
LANGCHAIN_TRACING=false
LANGCHAIN_TRACING_V2=false

When tracing is disabled, keep all four tracing flags set to false or unset in Dokploy. A stale LANGCHAIN_TRACING_V2=true / LANGCHAIN_TRACING=true value can still make LangSmith upload traces and surface Failed to send multipart request ... 403 Forbidden in the agents logs.

Dokploy's NEXT_PUBLIC_APP_URL is still required at runtime for server-side settings such as AUTH_URL, but client-visible URLs come from the GitHub repository variable used when the web image was built.

Dokploy-managed PostgreSQL is still self-hosted infrastructure. Before cutover, keep at least one restorable backup and plan scheduled backups, restore drills, disk usage monitoring, and connection/error log monitoring for production.

Step 6 — Deploy

Click Deploy in the Dokploy UI. Dokploy will:

  1. Pull the web, agents, migrations, and rbac-init images from GHCR
  2. Start redis
  3. Run the one-off migrations service against DATABASE_URL
  4. Run the one-off rbac-init service after migrations succeeds
  5. Start agents with LANGGRAPH_DATABASE_URL, then start web
  6. Inject Dokploy environment variables and wait for health checks

Step 7 — Apply Database Migrations

Database migrations run through the one-off migrations service in compose.yml:

migrations:
  image: ${GHCR_IMAGE_PREFIX:-ghcr.io/chenhaonan-eth/ugcad}-migrations:${IMAGE_TAG:-latest}

The image contains scripts/migrate-postgres.sh and the SQL files from apps/web/src/config/db/migrations. It uses the same IMAGE_TAG as web, agents, and rbac-init, so application code, RBAC initialization, and database schema stay aligned.

The migration image applies SQL files in apps/web/src/config/db/migrations/meta/_journal.json order and records successful tags in public.__ugcad_migrations. The first run creates the base schema from an empty PostgreSQL database; later runs skip migrations that were already applied. Do not copy SQL files into containers manually, and do not replace production SQL migrations with db:push.

If your Dokploy version does not automatically run one-off Compose services, run or restart the migrations service from the Dokploy UI and check its logs for applied or skipped entries before testing login.

For existing local/development databases, read apps/web/src/config/db/migrations/README.md before running migrations; do not confuse a local migration journal mismatch with a production Dokploy initialization failure.

Step 8 — Configure Domain and HTTPS

  1. In Dokploy, go to the web service → Domains
  2. Add your domain (e.g., app.yourdomain.com)
  3. Dokploy automatically provisions a Let's Encrypt certificate via Traefik

Make sure your DNS A record points to the server IP before enabling HTTPS.

Step 9 — Production Database Cutover Smoke Test

Do not treat container status alone as proof that the database cutover succeeded. Use this checklist to verify DATABASE_URL, LANGGRAPH_DATABASE_URL, RBAC init, business reads/writes, and rollback readiness.

migrations logs

  • The one-off migrations service completed successfully.
  • Fresh migrations printed applied on the new database.
  • Re-running the service prints skipped for tags already recorded in public.__ugcad_migrations.
  • Logs contain no SQL errors, auth failures, permission errors, or connection errors.
  • Rollout notes record the final applied / skipped migration tag.

RBAC init

  • The one-off rbac-init service completed after migrations.
  • A first deploy without RBAC_ADMIN_EMAIL initialized roles / permissions / role_permissions only.
  • If RBAC_ADMIN_EMAIL was set, the matching registered user received super_admin.
  • Admin users can access protected admin functionality.
  • Normal users cannot access admin-only functionality.
  • Anonymous users are redirected to auth or denied on protected pages.

Web runtime

# Web app
curl https://yourdomain.com/api/health
  • Public pages load.
  • https://yourdomain.com/sign-up and https://yourdomain.com/sign-in load.
  • /api/health returns a healthy response.
  • At least one production-enabled business write path succeeds, such as saving brand workspace settings or creating/updating a core business object.
  • Refreshing or logging in again reads back the written data from the business database pointed to by DATABASE_URL.

Agents / LangGraph runtime

# Agents API (internal only)
docker compose exec web wget -qO- http://agents:8000/ok
# → {"ok":true}
  • The agents service starts successfully.
  • Calling http://agents:8000/ok from the web container returns a successful response.
  • One agent flow that writes LangGraph runtime checkpoint/state/store data is exercised.
  • Logs or database inspection confirm runtime state uses LANGGRAPH_DATABASE_URL, not the business DATABASE_URL.

Brand workspace reads/writes

  • Open the active production brand workspace using BRAND_WORKSPACE_ACTIVE_BRAND_ID.
  • Read brand profile, assets, or core configuration.
  • Perform one representative write, such as saving brand configuration, updating asset metadata, or creating/updating a production-enabled core object.
  • Refresh and confirm the write persists.
  • Confirm the write did not land in the old Compose PostgreSQL volume.

Rollback readiness

  • The old Compose PostgreSQL volume or pre-cutover backup still exists.
  • Backup location, timestamp, and owner are recorded.
  • The old volume is not deleted before the observation window ends.
  • Rollback requires explicitly switching DATABASE_URL and LANGGRAPH_DATABASE_URL; there is no dual-write path.

Updating

Recommended flow: update by commit tag so the web, agents, migrations, and rbac-init images stay in sync.

  1. Push code to main or dev
  2. Wait for GitHub Actions to finish and confirm all four images were published with the same tag
  3. Update IMAGE_TAG in Dokploy to the new tag, preferably a sha-... tag; if you only changed runtime secrets, update the Dokploy environment and redeploy without rebuilding images
  4. Click Redeploy so Dokploy pulls the new images and reruns the migration and RBAC init services
  5. Check the migrations logs: new migrations print applied, already recorded ones print skipped
  6. Run the Step 9 smoke test checklist

Rollback is the same pattern in reverse for application code: set IMAGE_TAG back to the last known good tag and redeploy while keeping DATABASE_URL and LANGGRAPH_DATABASE_URL unchanged. If a new deployment already applied irreversible schema or data changes, image rollback alone may not be enough; restore from the database backup captured before cutover.

Keep the old Compose PostgreSQL volume for a short rollback window. If you must temporarily return to the embedded PostgreSQL service, enable the legacy-postgres Compose profile and explicitly set a strong POSTGRES_PASSWORD; the container refuses to start without it. After restoring both databases into the embedded PostgreSQL service, point DATABASE_URL at the restored business database and LANGGRAPH_DATABASE_URL at the restored LangGraph database before redeploying. Do not treat the old volume as a long-term production path, a dual-write target, or your only backup strategy.

If NEXT_PUBLIC_APP_URL changes, update the GitHub repository variable first, rebuild the web image, and then redeploy in Dokploy.

Troubleshooting

Dokploy cannot pull GHCR images

Confirm the image prefix and tag are correct:

GHCR_IMAGE_PREFIX=ghcr.io/chenhaonan-eth/ugcad
IMAGE_TAG=latest

If the package is private, configure credentials on the Dokploy server that can read the GHCR package.

Domain changed but the page still redirects to the old URL

NEXT_PUBLIC_APP_URL is a build-time variable. Update the GitHub repository variable, rerun GitHub Actions to publish a new web image, and then let Dokploy pull the new image.

agents cannot connect to PostgreSQL or Redis

Confirm LANGGRAPH_DATABASE_URL points to the LangGraph logical database and that the Dokploy app can reach that database host. If Dokploy shows both internal and external PostgreSQL URLs, prefer the internal URL inside the same Dokploy project/network. Redis still runs from compose.yml, so services must remain on the same Compose project network.

Web or migrations cannot connect to PostgreSQL

Confirm DATABASE_URL points to the business logical database, not the LangGraph database. Before saving it in Dokploy environment variables, keep Dokploy-provided TLS parameters such as sslmode and URL-encode special characters in credentials.

Web app shows 502 Bad Gateway

The agents container may not be ready yet. Check logs:

docker compose logs agents --tail=50

Schema errors after deploy

New migration files were added but the migrations service has not completed yet, or the first deploy has not run it. In Dokploy, check the migrations service logs and rerun that service. Successful runs print applied for new migrations and skipped for migrations already recorded in public.__ugcad_migrations.