Production Deployment

Production Deployment

Deploy ModulaCMS to a Linux server with automatic Let's Encrypt certificates, CI/CD via GitHub Actions, and systemd service management.

Prerequisites

  • A Linux server (AMD64) with root or sudo access
  • A domain name with DNS pointing to your server's IP address
  • Ports 80, 443, and your SSH port (default 2233) open in the firewall
  • Go 1.25+ with CGO enabled (for building from source)

Server Setup

Create the Deployment Directory

mkdir -p /root/app/modula
mkdir -p /root/app/modula/certs
mkdir -p /root/app/modula/logs
mkdir -p /root/app/modula/backups

chmod 755 /root/app/modula
chmod 700 /root/app/modula/certs
chmod 755 /root/app/modula/logs
chmod 755 /root/app/modula/backups

Create the systemd Service

Create /etc/systemd/system/modulacms.service:

[Unit]
Description=ModulaCMS Headless CMS
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/root/app/modula
ExecStart=/root/app/modula/modulacms-amd
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal

Environment="MODULACMS_ENV=production"

LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable modulacms
sudo systemctl start modulacms
sudo systemctl status modulacms

Configure the Firewall

sudo ufw allow 22/tcp    # SSH management
sudo ufw allow 80/tcp    # HTTP (required for Let's Encrypt challenge)
sudo ufw allow 443/tcp   # HTTPS
sudo ufw allow 2233/tcp  # ModulaCMS SSH TUI (adjust to match your ssh_port)
sudo ufw enable

Configure DNS

Before ModulaCMS can obtain Let's Encrypt certificates, your domain must resolve to the server:

dig +short your-domain.com
# Should return your server's IP address

dig +short admin.your-domain.com
# Should also return your server's IP address

Let's Encrypt requires your domain to be publicly accessible on port 80 for HTTP-01 challenge verification.

Configuration

Create /root/app/modula/modula.config.json:

{
  "environment": "production",
  "os": "linux",
  "environment_hosts": {
    "local": "localhost",
    "development": "localhost",
    "staging": "staging.your-domain.com",
    "production": "your-domain.com"
  },
  "port": ":80",
  "ssl_port": ":443",
  "cert_dir": "/root/app/modula/certs/",
  "client_site": "your-domain.com",
  "admin_site": "admin.your-domain.com",
  "ssh_host": "localhost",
  "ssh_port": "2233",
  "log_path": "/root/app/modula/logs/",
  "db_driver": "sqlite",
  "db_url": "/root/app/modula/modula.db",
  "db_name": "modula.db",
  "cors_origins": ["https://your-domain.com", "https://admin.your-domain.com"],
  "cors_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
  "cors_headers": ["Content-Type", "Authorization"],
  "cors_credentials": true,
  "cookie_secure": true,
  "cookie_samesite": "lax",
  "backup_option": "/root/app/modula/backups/",
  "update_auto_enabled": false,
  "update_check_interval": "startup",
  "update_channel": "stable"
}

Key Configuration Fields

Network and TLS:

Field Value Notes
environment "production" Enables automatic Let's Encrypt certificates
port ":80" Standard HTTP port
ssl_port ":443" Standard HTTPS port
cert_dir Path to cert directory Let's Encrypt stores certificates here
client_site Your domain Whitelisted for Let's Encrypt
admin_site Your admin subdomain Also whitelisted for Let's Encrypt

Database:

Field Value Notes
db_driver "sqlite", "mysql", or "postgres" Choose your database backend
db_url File path or connection string SQLite: file path. MySQL/PostgreSQL: connection string
db_name Database name Used with MySQL and PostgreSQL

Security:

Field Value Notes
auth_salt Auto-generated on first run Can be set manually for consistency across instances
cookie_secure true Required for HTTPS
cors_origins Array of allowed origins Use your actual domains

Optional -- S3 Storage: Configure bucket_region, bucket_media, bucket_endpoint, bucket_access_key, bucket_secret_key, and related fields if using S3-compatible storage for media.

Optional -- OAuth: Configure oauth_client_id, oauth_client_secret, and oauth_endpoint fields if enabling OAuth authentication with Google, GitHub, or Azure.

How HTTPS Works

When environment is set to anything other than "local", ModulaCMS automatically:

  1. Whitelists domains from environment_hosts[environment], client_site, and admin_site
  2. Obtains SSL certificates from Let's Encrypt on the first HTTPS request
  3. Stores certificates in cert_dir
  4. Renews certificates automatically before expiration
  5. Serves both HTTP (port 80) and HTTPS (port 443) concurrently

No reverse proxy or manual certificate renewal is needed.

Deploying

Automatic Deployment via GitHub Actions

Push to the dev or develop branch to trigger automatic deployment. The workflow runs tests, builds a Linux AMD64 binary, deploys it to your server via SSH, and restarts the service.

Configure these GitHub repository secrets (Settings > Secrets and variables > Actions):

Required:

Secret Description Example
DEPLOY_SSH_KEY SSH private key for the deployment server Contents of ~/.ssh/modulacms_deploy
DEPLOY_HOST Server hostname or IP your-server.com
DEPLOY_USER SSH username root

Optional:

Secret Description Default
DEPLOY_PATH Deployment directory on server /root/app/modula
HEALTH_CHECK_URL URL for post-deploy health check (none)

Generate a deployment SSH key:

ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/modulacms_deploy
ssh-copy-id -i ~/.ssh/modulacms_deploy.pub root@your-server.com

Copy the private key contents into the DEPLOY_SSH_KEY secret.

Manual Deployment

Build and deploy from your local machine:

just build

Or build and transfer manually:

# Build for Linux AMD64
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -mod vendor -o modulacms-amd ./cmd

# Transfer to server
scp modulacms-amd root@your-server.com:/root/app/modula/

# Restart the service
ssh root@your-server.com "sudo systemctl restart modulacms"

Creating Releases

Tag a version to create a GitHub release with binaries for all platforms:

git tag -a v1.0.0 -m "Release version 1.0.0"
git push origin v1.0.0

The CI workflow builds for darwin/linux on amd64/arm64, creates a GitHub release, and generates release notes.

Monitoring

Service Status

sudo systemctl status modulacms

Logs

# Follow live logs
sudo journalctl -u modulacms -f

# Last 100 lines
sudo journalctl -u modulacms -n 100

# Logs from the last hour
sudo journalctl -u modulacms --since "1 hour ago"

Rollback

If a deployment introduces problems, restore the backup binary:

ssh root@your-server.com
sudo systemctl stop modulacms
cd /root/app/modula
cp modulacms-amd.backup modulacms-amd
sudo systemctl start modulacms
sudo systemctl status modulacms

Troubleshooting

SSH Authentication Failure During Deploy

Permission denied (publickey) during CI deployment.

  • Verify the SSH private key is correctly stored in the DEPLOY_SSH_KEY GitHub secret
  • Confirm the public key is in ~/.ssh/authorized_keys on the server
  • Check permissions: chmod 600 ~/.ssh/authorized_keys

Service Fails to Start

Check the journal for details:

sudo journalctl -u modulacms -n 50 --no-pager

Common causes:

  • Ports in use: sudo lsof -i :80 and sudo lsof -i :443
  • Missing database file: ls -la /root/app/modula/modula.db
  • Permission issues: chmod +x /root/app/modula/modulacms-amd

Build Fails with CGO Errors

Cross-compiling CGO code (required for SQLite) needs a C cross-compiler:

# On macOS targeting Linux
brew install FiloSottile/musl-cross/musl-cross

Alternatively, build directly on the target Linux server.

Security Recommendations

  • Use a dedicated deployment user instead of root
  • Restrict the deployment SSH key to deployment operations only
  • Rotate deployment SSH keys every 6 months
  • Never commit credentials to the repository
  • Enable the firewall and only allow necessary ports
  • Monitor logs for unauthorized access attempts

Reverse Proxy (Optional)

ModulaCMS handles HTTPS natively, so a reverse proxy is not required. If you need one for load balancing, additional security layers, or serving multiple applications on the same server, place Caddy or Nginx in front of ModulaCMS. See the deploy/Caddyfile in the repository for an example configuration.