Sign inSign up

lthub/canvas-dev-only

By lthub

Updated 7 months ago

Instructure Canvas LMS, built for local development use only. Not a single image service.

Image
Content management system
0

1.4K

lthub/canvas-dev-only repository overview

Expected Credentials

  • Admin Username: [email protected]

  • Admin Password: password

  • Admin API Key - depends on which image you use

    • 2025-04-23: Tz4aawtEPDNLTtHXNAPkW4N9my8Qw2faWyueTT8fPR88NvcfQT3TMD8vPAm4AUQn
    • 2025-06-18 and above: YPhZLv6wyABWUeEKMz99VGQvwNmETtZ8eerYHf4Afzv6uR8m4kAWnvD2wBCKkAF7
  • Postgres Username: postgres

  • Postgres Password: sekret

  • Postgres Database Name: canvas_development

The docker compose snippet below should bring up a Canvas instance accessible at http://localhost:9100/

Docker Compose

NOTE: For ARM users (e.g.: Apple Silicon Macs), use the arm variant of the image tags, e.g.: lthub/canvas-dev-only:release.2026-01-28.618.arm

This example expects you to copy the database dump from within the Canvas image in /usr/src/app/dbinit/ to a local directory docker/dev/canvas/. The database dump sql files are executed when postgres starts up with an empty database to initialize Canvas. Example command to copy the database base to host:

docker create --name tmpCanvas lthub/canvas-dev-only:release.2026-01-28.618
mkdir -p docker/dev/canvas
docker cp tmpCanvas:/usr/src/app/dbinit/. docker/dev/canvas/
docker rm -f tmpCanvas

Example docker-compose.yml file below for running this dev instance.

# Only for dev use
services:
  canvas.postgres:
    image: pgvector/pgvector:pg14
    volumes:
      - canvasPostgresData:/var/lib/postgresql/data
      - ./docker/dev/canvas:/docker-entrypoint-initdb.d
    healthcheck: # no built-in healthcheck in the image, so we specify one
      test: ["CMD-SHELL", "pg_isready -U postgres -h localhost -p 5432"]
      start_period: 2m # ignore failures in the initial 2 minutes
    environment:
      POSTGRES_PASSWORD: sekret

  canvas: &CANVAS
    image: lthub/canvas-dev-only:release.2026-01-28.618
    ports:
      - 9100:80
    depends_on:
      canvas.postgres:
        # if starting from scratch, postgres db needs to populated, which can
        # take some time, so we need to wait until postgres is healthy
        condition: service_healthy
      canvas.redis:
        condition: service_started
    volumes:
      - api_docs:/usr/src/app/public/doc/api
      - brandable_css_brands:/usr/src/app/app/stylesheets/brandable_css_brands
      - bundler:/home/docker/.bundle/
      - canvas-docker-gems:/home/docker/.gem/
      - js-utils_es:/usr/src/app/packages/js-utils/es
      - js-utils_lib:/usr/src/app/packages/js-utils/lib
      - js-utils_node_modules:/usr/src/app/packages/js-utils/node_modules
      - locales:/usr/src/app/config/locales/generated
      - log:/usr/src/app/log
      - node_modules:/usr/src/app/node_modules
      - pacts:/usr/src/app/pacts
      - public_dist:/usr/src/app/public/dist
      - reports:/usr/src/app/reports
      - styleguide:/usr/src/app/app/views/info
      - tmp:/usr/src/app/tmp
      - translations:/usr/src/app/public/javascripts/translations
      - yardoc:/usr/src/app/.yardoc
      - yarn-cache:/home/docker/.cache/yarn
    environment:
      VIRTUAL_HOST: .canvas.docker
      CANVAS_DATABASE_HOST: canvas.postgres
      CANVAS_REDIS_HOST: canvas.redis
      HTTPS_METHOD: noredirect
      POSTGRES_PASSWORD: sekret
      ENCRYPTION_KEY: facdd3a131ddd8988b14f6e4e01039c93cfa0160
      RAILS_ENV: development

  canvas.job:
    <<: *CANVAS
    command: bundle exec script/delayed_job run
    attach: false # job is very noisy, so we hide logs for it by default
    ports: []

  canvas.redis:
    image: redis:alpine

  # Inside the docker network, canvas is served on port 80. When we expose
  # canvas to external traffic however, we serve canvas on port 9100. This
  # mismatch means that when app talks directly to canvas using the external
  # url (http://localhost:9100), it doesn't work because app is also inside
  # the docker network and needs to use port 80. To workaround this, socat will
  # redirect internal docker traffic to port 9100 to canvas port 80.
  canvas.socat:
    image: alpine/socat:1.8.0.3
    depends_on:
      - canvas
    command: "TCP-LISTEN:9100,fork,reuseaddr TCP:canvas:80"

volumes:
  canvasPostgresData: {}
  api_docs: {}
  brandable_css_brands: {}
  bundler: {}
  canvas-docker-gems: {}
  i18nliner_node_modules: {}
  js-utils_es: {}
  js-utils_lib: {}
  js-utils_node_modules: {}
  k5uploader_es: {}
  k5uploader_lib: {}
  k5uploader_node_modules: {}
  locales: {}
  log: {}
  node_modules: {}
  pg_data: {}
  pacts: {}
  public_dist: {}
  reports: {}
  styleguide: {}
  tmp: {}
  translations: {}
  yardoc: {}
  yarn-cache: {}

Refer to 'Expected Credentials' section above for URL and admin credentials.

Note that I use the hosts file to map the domain 'canvas.docker' to 127.0.0.1, so I can use http://canvas.docker:9100/ to access Canvas. Some parts of the docker compose file uses this canvas.docker domain, if you encounter unexpected hostname issues when using http://localhost:9100, it might be due to this.

Canvas Image

To build a new version, first checkout the Canvas LMS repo and switch to a release tag. The release tags are dated, avoid releases that have not yet been deployed and be cautious of the current deployed release. Future release tags might not build properly, and even the currently deployed tags might have bugs that's being patched. For example, if we're currently on 2025-07-02 release tag, both the current 2025-07-02 tags and the future 2025-07-16 tags might have issues, and Instructure is probably still adding commits to them. It might be safer to use the release before 2025-07-02, which is 2025-06-18.

Once you're on a release tag, run the Canvas setup script manually ./script/docker_dev_setup.sh to get a working running Canvas. The Canvas setup script unfortunately does not use Bundler's deployment build option, so it might try to modify the lockfile and run into a permission issue. This is one factor in why it can be difficult to reproduce Canvas builds. See Troubleshooting section for more details.

  • You can skip dory as we're not using it
  • For first run, answer yes to 'Copying Canvas docker configuration'. You can answer no for any subsequent runs, including skipping resetting the .env file.

Once you get docker_dev_setup.sh to complete successfully, then you can build a new image using Canvas' own Dockerfile.production, with the following modifications:

  • Editing config/redis.yml to replace redis://redis with redis://<%= ENV.fetch('CANVAS_REDIS_HOST', 'redis') %> so the redis host can be configured via env vars.

  • Create config/environments/development-local.rb with the content below. This file disables Canvas not allowing requests to local IPs as a security measure. Without it, local development has issues (e.g.: course > people ajax loading, grabbing JWKS from a locally running app):

    Rails.application.configure do
      # remove blocking local ips in dev (in particular 10.0.0.1/8,
      # 192.168.0.0/16), this breaks ajax loading (e.g.: course > people) and makes
      # Canvas unable to get JWKS from other docker compose services
      CanvasHttp.blocked_ip_ranges = []
    end
    
  • Editing config/database.yml to replace <%= ENV.fetch('POSTGRES_PASSWORD') %> with <%= ENV.fetch('POSTGRES_PASSWORD', 'postgres') %>, this fix a build issue that complains POSTGRES_PASSWORD is null.

We want to use the prod Dockerfile so we have the compiled static dependencies and don't need to run a separate webpack container for them.

To build the image, docker-compose.override.yml was edited to add an 'app' section for building the image as follows:

app:
  build:
    context: .
    dockerfile: Dockerfile.production
  image: lthub/canvas-dev-only:release.2025-04-23.348
  environment:
    VIRTUAL_HOST: .canvas.docker
    HTTPS_METHOD: noredirect
    POSTGRES_PASSWORD: sekret
    ENCRYPTION_KEY: facdd3a131ddd8988b14f6e4e01039c93cfa0160

The image can then be build using docker compose build app and pushed to Dockerhub using docker compose push app. Remember to update the image tag as necessary.

Database dump was taken inside the Postgres container and copied to dbinit/ inside the Canvas image. You will need to mount these sql files to your own Canvas's Postgres init scripts directory in order to properly initialize the Canvas database. The database dump commands:

pg_dumpall -U postgres -W --globals-only --file=01_globals.sql
pg_dump -U postgres -W --create canvas_development > 02_canvas_development.sql
pg_dump -U postgres -W --create canvas_test > 03_canvas_test.sql

Edit the 01_globals.sql file to remove the creation of the role postgres as that'll be automatically created by the postgres container.

We won't use the canvas_test database, it's included to make migration to newer Canvas versions easier. Instructure's docker_canvas_setup.sh script's db migration step expects both canvas_development and canvas_test databases to exist.

The Canvas postgres service in your app's docker-compose.yml should look something like this, we need the pgvector plugin, so we're using their image:

canvas.postgres:
  image: pgvector/pgvector:pg14
  volumes:
    - canvasPostgresData:/var/lib/postgresql/data
    - ./path/to/canvas/sql:/docker-entrypoint-initdb.d
  environment:
    POSTGRES_PASSWORD: sekret

Additional Changes

  • Populated permissions & roles to match our prod Canvas.
  • Change default timezone for root account, Site Admin, [email protected] to Pacific time
  • [email protected] has Integration ID set (so apps requiring PUID don't complain)
  • Stock LTI keys are too old, and thus too short, to satisfy RSA256 requirements. If this is a completely new build, you need to force Canvas to rotate them out before taking new DB dumps. You can check the current LTI keys at the JWKS url: http://localhost:9100/api/lti/security/jwks
    • There is a monthly cron job (1st of every month) that gets executed to rotate the LTI keys, it is configured in: config/initializers/periodic_jobs.rb. The job is Lti::KeyStorage.rotate_keys. We can find the job this creates in the delayed_jobs table:

      SELECT * FROM "delayed_jobs" where handler like '%rotate_keys%';
      

      We can change the 'run_at' field so it'll run sooner. Not sure if needed, but best to restart Canvas after changing the db in case that's needed for the change to be picked up. Warning: The rotate keys job enforces 1 hour of separation between rotates, so it'll do nothing if you've already rotated keys once in the past hour. There are 3 old keys to rotate out, so this requires at least 3 hours.

      Template:

      UPDATE "delayed_jobs" SET run_at = '<INSERT DATETIME>' WHERE id=<INSERT ID>;
      

      Example:

      UPDATE "delayed_jobs" SET run_at = '2025-11-17 08:58:00' WHERE id=2793;
      

Troubleshooting

File Permission Issue While Building Image

You might get a permission error about writing to the Gemfile.lock or similar. Instructure uses a user/group with a uid/gid of 9999 to run all their containers. So one way we can sidestep the permission issue is to set all Canvas files to be owned by this Instructure docker user/group. In my case, I created a instructure-docker group with a gid of 9999 and set the entire Canvas source directory to be owned by that group.

Alternatively, edit scripts/docker_dev_setup.sh and comment out the CANVAS_SKIP_DOCKER_USERMOD declaration:

#CANVAS_SKIP_DOCKER_USERMOD='true'

This will instead change the Instructor docker user's uid to your own uid.

Unable to migrate to newer database

If the failure is on 20251008075319_add_access_ignite_agent_permission.rb or similar AI permission migration, then the issue may be the custom roles and permissions copied over from Canvas prod. Might be a configuration thing, but the AI migrations seem to be expecting hardcoded roles that doesn't exist for us, even when creating the Canvas database from scratch.

Edit dbinit/02_canvas_development.sql and delete the section that copies all the UBC roles and role overrides. The role overrides section should start with COPY public.role_overrides. The roles section should start with COPY public.roles, as of 2025-10-08, the roles with id 1 to 16 are the default roles, delete the non-default roles (id 17 and up). Reset the database with the updated sql file and try the migration again.

Unfortunately, this does require recopying the roles and permissions from prod Canvas again.

Tag summary

Content type

Image

Digest

sha256:4f60d7348

Size

2.8 GB

Last updated

7 months ago

docker pull lthub/canvas-dev-only:release.2026-01-28.626.arm