Deployment
Self-Host with Docker

Use this guide if you want to run rtSurvey on your own server (VPS, bare metal, or local machine) using the public Docker image rtawebteam/rtcloud:survey-public.

No Docker Hub login required — the image is public.

The container bundles everything in a single image:

  • Apache + PHP (application server)
  • Shiny Server (analytics dashboards, port 3838)
  • Beanstalkd (background job queue)

Requirements

Server

ResourceMinimumRecommended
RAM4 GB8 GB
Disk50 GB100 GB SSD
CPU2 vCPUs4 vCPUs
OSUbuntu 22.04 LTSUbuntu 22.04 LTS

Software


Setup

Create a working directory

mkdir -p /opt/rtsurvey && cd /opt/rtsurvey

Create docker-compose.yml

services:
  mysql:
    image: mysql:8.0
    container_name: rtsurvey-mysql
    restart: unless-stopped
    command: >
      --default-authentication-plugin=mysql_native_password
      --character-set-server=utf8
      --collation-server=utf8_unicode_ci
      --sql-mode=NO_ENGINE_SUBSTITUTION
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE:-smartsurvey}
      MYSQL_USER: ${MYSQL_USER:-smartsurvey}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_ROOT_HOST: '%'
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - rtsurvey-net
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
 
  rtsurvey:
    image: rtawebteam/rtcloud:survey-public
    container_name: rtsurvey-app
    restart: unless-stopped
    entrypoint: ["/bin/entrypoint-production.sh"]
    depends_on:
      mysql:
        condition: service_healthy
    ports:
      - "${APP_PORT:-8080}:80"
      - "${SHINY_PORT:-3838}:3838"
    env_file:
      - .env
    volumes:
      - app_uploads:/var/www/html/smartsurvey/uploads
      - app_audios:/var/www/html/smartsurvey/audios
      - app_downloads:/var/www/html/smartsurvey/downloads
      - app_gallery:/var/www/html/smartsurvey/gallery
      - app_tmp:/var/www/html/smartsurvey/tmp
      - app_cache:/var/www/html/smartsurvey/cache
      - app_runtime:/var/www/html/smartsurvey/protected/runtime
      - app_assets:/var/www/html/smartsurvey/assets
      - app_aggregate:/var/www/html/smartsurvey/aggregate
    networks:
      - rtsurvey-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 90s
 
  # Optional: Embedded Keycloak SSO
  # Start with: docker compose --profile embed-keycloak up -d
  keycloak:
    image: quay.io/keycloak/keycloak:latest
    container_name: rtsurvey-keycloak
    restart: unless-stopped
    profiles:
      - embed-keycloak
    command: start --import-realm
    environment:
      KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN_USER:-admin}
      KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
      KC_HTTP_ENABLED: "true"
      KC_HTTP_RELATIVE_PATH: /auth
      KC_PROXY_HEADERS: xforwarded
      KC_HOSTNAME: ${KC_HOSTNAME}
      KC_HOSTNAME_STRICT: "false"
      KC_DB: mysql
      KC_DB_URL_DATABASE: ${KEYCLOAK_DB:-keycloak}
      KC_DB_URL_HOST: mysql
      KC_DB_USERNAME: ${KEYCLOAK_DB_USER:-keycloak}
      KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD}
    volumes:
      - ./keycloak-import:/opt/keycloak/data/import
    ports:
      - "${KEYCLOAK_PORT:-8091}:8080"
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - rtsurvey-net
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:9000/health/ready || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 10
      start_period: 120s
 
volumes:
  mysql_data:
  app_uploads:
  app_audios:
  app_downloads:
  app_gallery:
  app_tmp:
  app_cache:
  app_runtime:
  app_assets:
  app_aggregate:
 
networks:
  rtsurvey-net:
    driver: bridge

Create .env

# Project
PROJECT_ID=mysurvey
PROJECT_TYPE=rtsurvey
PROJECT_URL=your-server-ip-or-domain
HTTP_PROTOCOL=http
TZ=UTC
 
# Database
MYSQL_HOST=mysql
MYSQL_DATABASE=smartsurvey
MYSQL_USER=smartsurvey
MYSQL_ROOT_PASSWORD=change-this-root-password
MYSQL_PASSWORD=change-this-app-password
 
# Admin (set before first startup — cannot change via env after DB is created)
ADMIN_PASSWORD=change-this-admin-password
 
# Runtime
RUN_ENV=prod
RUN_MODE=admin
GII_ENABLED=false
CSRF_VALIDATION_ENABLED=true
 
# Ports (optional — defaults shown)
APP_PORT=8080
SHINY_PORT=3838
⚠️

Change MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD before starting. Use strong random strings (32+ characters) in production.

Start the stack

docker compose up -d

The first startup takes about 60–90 seconds — MySQL initializes the database on the first run.

Verify

# Check all containers are running
docker compose ps
 
# Check application health
curl http://localhost:8080/health

First Login

Once the health check passes, open your browser:

http://your-server:8080/cpms/cpmsSite/login
FieldValue
Usernameadmin
Passwordvalue of ADMIN_PASSWORD in your .env (defaults to admin)
⚠️

Set ADMIN_PASSWORD in your .env before first startup. The entrypoint sets it on first run — changing it after the database is initialized has no effect.


Environment Variables

Required by the app container

The entrypoint will refuse to start if any of these are missing:

VariableDescription
PROJECT_IDUnique identifier for this instance (alphanumeric, no spaces)
PROJECT_URLDomain or IP where the app is accessed (no http:// prefix)
MYSQL_HOSTMySQL hostname — use the Docker service name (e.g. mysql)
MYSQL_DATABASEDatabase name
MYSQL_PASSWORDDatabase user password

Required by the MySQL container

VariableDescription
MYSQL_ROOT_PASSWORDMySQL root password (used by the mysql container, not the app)

Recommended

VariableDefaultDescription
MYSQL_USERsmartsurveyDatabase username
ADMIN_PASSWORDadminInitial admin password — set before first startup, cannot change via env after DB is created
HTTP_PROTOCOLhttpsSet to http if not using SSL
PROJECT_PORT80External port users connect to (not the internal Docker port)
PROJECT_TYPErtworkPlatform type — use rtsurvey for the survey platform
TZUTCTimezone (e.g. Asia/Bangkok, America/New_York)

Runtime

VariableDefaultDescription
RUN_ENVprodEnvironment mode (prod or dev)
RUN_MODEadminService role — use admin for a single-server setup
GII_ENABLEDfalseKeep false in production (Yii code generator)
CSRF_VALIDATION_ENABLEDtrueCSRF protection — keep true

License

VariableDefaultDescription
REQUIRE_LICENSEfalseSet to true to enforce license validation
RTCLOUD_LICENSE_KEYYour license key — required when REQUIRE_LICENSE=true

Data Persistence

All user data is stored in named Docker volumes. These survive container restarts and image updates.

VolumeContents
mysql_dataDatabase (surveys, users, responses)
app_uploadsFile uploads from surveys
app_galleryMedia library
app_downloadsGenerated export files
app_audiosAudio recordings
app_runtimePHP session and cache files
app_aggregateAggregated analytics data

Common Operations

View logs

docker compose logs -f rtsurvey
docker compose logs -f mysql

Restart the app

docker compose restart rtsurvey

Access a shell

docker compose exec rtsurvey bash

Run database migrations manually

docker compose exec rtsurvey php /var/www/html/smartsurvey/protected/yiic migrate

Upgrades

The image is tagged survey-public. To update to the latest version:

docker compose pull
docker compose up -d

Database migrations run automatically on startup via the production entrypoint.


Backup & Restore

Backup database

docker compose exec mysql \
  mysqldump -u root -p"${MYSQL_ROOT_PASSWORD}" smartsurvey \
  > backup-$(date +%Y%m%d).sql

Restore database

docker compose exec -T mysql \
  mysql -u root -p"${MYSQL_ROOT_PASSWORD}" smartsurvey \
  < backup-20240101.sql

Backup uploaded files

docker run --rm \
  -v rtsurvey_app_uploads:/data \
  -v $(pwd):/backup \
  alpine tar czf /backup/uploads-$(date +%Y%m%d).tar.gz -C /data .

Embedded Keycloak (Optional SSO)

The production compose file includes an optional Keycloak container for SSO. It shares the same MySQL instance and is activated via a Docker Compose profile.

Embedded Keycloak requires at least 4 GB RAM. It is optional — skip this if you don't need SSO.

Add Keycloak vars to .env

# Keycloak admin
KEYCLOAK_ADMIN_USER=admin
KEYCLOAK_ADMIN_PASSWORD=change-this-keycloak-password
 
# Keycloak database (created automatically in the shared MySQL on first run)
KEYCLOAK_DB=keycloak
KEYCLOAK_DB_USER=keycloak
KEYCLOAK_DB_PASSWORD=change-this-keycloak-db-password
 
# KC_HOSTNAME: full public URL of your server (Keycloak appends /auth automatically)
KC_HOSTNAME=http://your-server:8091
 
# Port Keycloak listens on (host)
KEYCLOAK_PORT=8091
 
# Tell the rtCloud app to connect to embedded Keycloak
EMBED_KEYCLOAK=true
KEYCLOAK_URL=http://your-server:8091
KEYCLOAK_REALM=rtsurvey
KEYCLOAK_CLIENT_ID=rtsurvey
KEYCLOAK_CLIENT_SECRET=your-client-secret

The keycloak service is already included in the docker-compose.yml above (under the embed-keycloak profile — it won't start unless you use --profile embed-keycloak).

Create the import directory (Keycloak will start without it but realm auto-provisioning won't work):

mkdir -p /opt/rtsurvey/keycloak-import

Start with Keycloak

docker compose --profile embed-keycloak up -d

To start without Keycloak (default):

docker compose up -d

Keycloak admin console

Once healthy, access the Keycloak admin UI at:

http://your-server:8091/auth/admin

Login with KEYCLOAK_ADMIN_USER / KEYCLOAK_ADMIN_PASSWORD.


Next Steps

  • HTTPS — Put nginx or Caddy in front and follow the SSL Setup guide
  • SSO — Connect Azure AD or Keycloak via the SSO Authentication guide
  • Cloud deployment — See Cloud Providers for provider-specific setup with automated scripts