Skip to content

Docker Deployment

Deploy Deckyard using Docker and Docker Compose for easy setup and management.

  • Docker - Version 20.10 or later
  • Docker Compose - Version 2.0 or later
  • Domain name - For HTTPS (production)
  • DNS configured - Pointing to your server
Terminal window
git clone https://github.com/jaapstronks/deckyard.git
cd deckyard

Copy the example environment file:

Terminal window
cp .env.example .env

Edit .env with your configuration:

Terminal window
# Required unless you set AUTH_ENABLED=false (at least 32 characters)
AUTH_SECRET=your-random-secret-key-here
AUTH_ADMIN_EMAIL=admin@yourdomain.com
# Domain (for Caddy HTTPS)
DOMAIN=slides.yourdomain.com
LETSENCRYPT_EMAIL=admin@yourdomain.com
# Optional: Email
BREVO_API_KEY=your-brevo-api-key
BREVO_SENDER_EMAIL=slides@yourdomain.com
BREVO_SENDER_NAME=Deckyard
# Optional: AI features
OPENAI_API=sk-...
# Optional: Media storage
IMAGEKIT_PUBLIC_KEY=...
IMAGEKIT_PRIVATE_KEY=...
IMAGEKIT_URL_ENDPOINT=...

The server refuses to start without an AUTH_SECRET of at least 32 characters unless you opt out of authentication with AUTH_ENABLED=false (see Authentication). On the first start, with an empty user table, the server creates AUTH_ADMIN_EMAIL as the first admin, who signs in with a magic link or through SSO (see Your first admin). The database needs no settings: the stack brings its own.

Terminal window
docker compose up -d

This starts:

  • app - The Deckyard application on port 4177 (reachable only on the compose network)
  • postgres - PostgreSQL 16, the database; migrations run automatically when the app container starts
  • caddy - Reverse proxy with automatic HTTPS

Open https://your-domain.com in your browser.

docker-compose.yml in the repository is the production topology: app behind caddy on ports 80 and 443, with postgres next to it. The app only exposes port 4177 on the compose network, so a bare docker compose up does not publish http://localhost:4177. For a local run without a domain, add the local overlay, which publishes 4177 (and the database on 5433) and leaves Caddy out:

Terminal window
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build

Every DATABASE_* value in the compose file can be overridden from .env; Database Configuration explains the defaults and how to use a managed database instead.

VolumePurpose
pg_dataThe PostgreSQL database: presentations, users, settings and everything else except media
./server/uploadsUploaded images and files
./server/dataDeck-thumbnail cache, plus files left by an install from before PostgreSQL
caddy_dataSSL certificates and Caddy state
caddy_configCaddy configuration cache
{
email {$LETSENCRYPT_EMAIL}
}
{$DOMAIN} {
encode gzip
reverse_proxy app:4177
}

Caddy automatically:

  • Obtains SSL certificates from Let’s Encrypt
  • Renews certificates before expiry
  • Redirects HTTP to HTTPS
  • Enables gzip compression

Set these in your .env file:

Terminal window
DOMAIN=slides.yourdomain.com
LETSENCRYPT_EMAIL=admin@yourdomain.com

To serve multiple domains:

{
email {$LETSENCRYPT_EMAIL}
}
slides.example.com, presentations.example.com {
encode gzip
reverse_proxy app:4177
}

The included Dockerfile builds on node:22-alpine and adds:

  • Chromium (plus an emoji font) - for server-side PNG, PDF and PPTX exports
  • A non-root user - the app runs as the image’s node user
  • An entrypoint - applies pending database migrations, then starts the server

Add resource limits in docker-compose.yml:

services:
app:
# ... other config
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M

The image has its own HEALTHCHECK on GET /health (no login, no database), so Docker, Coolify or another orchestrator knows when the container serves without any configuration. docker compose ps shows the result. Override it in docker-compose.yml only if you need other intervals.

/health says the process is up; npm run doctor says it is configured right. It checks the installation without starting the server: auth secret, public URL, database and migrations, SSO and the provider’s discovery document, themes, Chrome for exports, uploads and mail. One line per check, and for anything red, the fix.

Terminal window
docker compose exec app node scripts/doctor.js # in the running container
npm run doctor # in a checkout, reads .env
npm run doctor -- --json # the same findings as JSON

It exits with 1 when a check failed, so a deploy step can gate on it; warnings do not fail. Run it after editing .env, before the first user, and after every update. It writes nothing lasting: the uploads check writes two probe files and deletes them again.

Configure log rotation:

services:
app:
# ... other config
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

The stack’s own postgres service is the default. To use a managed or existing PostgreSQL instead, set its connection in .env, for example:

Terminal window
DATABASE_URL=postgres://user:pass@your-db-host:5432/deckyard

Do not set DATABASE_HOST=localhost here: inside the container that is the app itself, and the entrypoint refuses to start. See Database Configuration for SSL and pool settings.

Terminal window
git pull origin main
docker compose build
docker compose up -d

Pending migrations run when the new app container starts. Then run docker compose exec app node scripts/doctor.js to confirm the configuration still passes; read the release notes for renamed settings first.

To rebuild and restart the app without touching the database and Caddy containers:

Terminal window
docker compose up -d --no-deps --build app

The app is briefly unavailable while its container is replaced.

Back up the database and the media together, the media right after the dump:

Terminal window
docker compose exec -T postgres sh -c \
'pg_dump -Fc --no-owner --no-acl -U "$POSTGRES_USER" "$POSTGRES_DB"' \
> deckyard-$(date +%F).dump
tar czf deckyard-uploads-$(date +%F).tgz server/uploads server/data/private-media

server/data/private-media holds uploaded fonts, which the database refers to; the rest of server/data is a thumbnail cache. How to restore, and how to rehearse a restore, is on Back Up and Restore.

Terminal window
# All services
docker compose logs -f
# Specific service
docker compose logs -f app
docker compose logs -f caddy
Terminal window
docker compose ps
Terminal window
docker compose exec app sh

If Caddy fails to get certificates:

  1. Check domain DNS is pointing to your server
  2. Verify ports 80 and 443 are open
  3. Check Caddy logs: docker compose logs caddy

If the app crashes due to memory:

  1. Increase memory limits in docker-compose.yml
  2. Use external media storage

For a local run with Docker, use the local overlay described under Default Configuration:

Terminal window
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build

Or run without Docker for faster iteration.