Tutorial

Laravel on a Container Host Without a VPS: One Dockerfile, a Real Database, Honest Queues

SnapDeploy Team 2026-10-21 Updated 2026-10-21 8 min read
laravelphpdockermysqlfree-tier

Laravel's deployment story has always assumed a server: Forge, a VPS, nginx and PHP-FPM you configure once and forget. A container host removes the server but not the question of what serves PHP. This guide gets a Laravel 13 app onto a free container tier with one Dockerfile, a real database and honest answers about queues, caches and uploads, none of which behave the way they do on a VPS.

Laravel 13 (released 17 March 2026) requires PHP 8.3 or newer; Laravel 12 runs on 8.2 and still receives security fixes until February 2027. The Dockerfile below uses PHP 8.3 and works for both.

Bring your own Dockerfile, and here is why

SnapDeploy generates Dockerfiles for several languages, and for PHP it produces a php:8.2-fpm image that installs Composer and your dependencies. That image runs PHP-FPM, which speaks FastCGI, not HTTP; it is built to sit behind a web server, and the generated file does not start one. It also uses PHP 8.2, which Laravel 13 no longer accepts. So for Laravel the right move is a Dockerfile in the repository, which SnapDeploy then uses as-is, reading the port from its EXPOSE line. The simplest single-process option is the official Apache image with mod_php:

FROM php:8.3-apache
RUN apt-get update && apt-get install -y --no-install-recommends libpq-dev libzip-dev unzip && rm -rf /var/lib/apt/lists/*
RUN docker-php-ext-install pdo_mysql pdo_pgsql zip opcache
RUN a2enmod rewrite && sed -i 's#/var/www/html#/var/www/html/public#g' /etc/apache2/sites-available/000-default.conf /etc/apache2/apache2.conf
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction
COPY . .
RUN composer dump-autoload --optimize && chown -R www-data:www-data storage bootstrap/cache
ENV APP_ENV=production APP_DEBUG=false LOG_CHANNEL=stderr
EXPOSE 80
CMD ["sh", "-c", "php artisan config:cache && php artisan route:cache && php artisan view:cache && php artisan migrate --force && apache2-foreground"]

Apache with mod_php is not the fastest PHP server, but for a free container it is the least surprising: one process, port 80, no supervisor, and opcache does most of the performance work. If you want more throughput later, FrankenPHP (a Go web server with PHP embedded, the engine behind Laravel Octane's newest driver) is the same single-container shape with better numbers, at the cost of a less familiar setup.

The caches are built at start rather than at build time because config:cache bakes environment variables into the cache, and the real values only exist in the running container. LOG_CHANNEL=stderr sends the Laravel log to the container log, where you can actually read it.

Environment variables

Variable Value Note
APP_KEYfrom php artisan key:generate --showNever commit it; without it every session and encrypted value is unreadable.
APP_URLhttps://<name>.containers.snapdeploy.appUsed for generated links and signed URLs.
DB_CONNECTION, DB_URLmysql or pgsql, plus the connection stringA SnapDeploy MySQL or PostgreSQL Mini ($29/month) injects DATABASE_URL; map it to DB_URL or set the parts. Neon's free Postgres works too.
SESSION_DRIVERdatabase or cookieNot file: the filesystem is ephemeral and a sleep or deploy logs everyone out.
CACHE_STOREdatabase, or redis with a Redis Mini ($12/month)file also works because a cache may be lost; it is just slower.
QUEUE_CONNECTIONsync on the free tierSee the next section.
FILESYSTEM_DISKs3Uploads on the local disk disappear with the container.

Queues on a container that sleeps

On a VPS you run php artisan queue:work under Supervisor and forget it. On a free container tier the obvious translation, a second container running the worker, does not work: a worker container receives no HTTP requests, so after 15 minutes the platform considers it idle and puts it to sleep. Jobs would process for a quarter of an hour after each deploy and then stall until someone restarted it.

The honest options, in order: QUEUE_CONNECTION=sync, which runs jobs inline in the request (fine for emails and small tasks on a hobby app); a database queue drained by a route you call from an external scheduler, which is a workaround rather than a design; or Always-On ($12/month per container), where a worker container simply keeps running. Scheduled tasks (schedule:run) have the same shape: on the free tier they only fire while the web container is awake, so use them for housekeeping that can wait for the next visitor, not for anything time-critical.

Memory and start-up

Apache with a handful of PHP workers and opcache uses 60 to 120 MB for a typical Laravel app, so 512 MB is comfortable; set MaxRequestWorkers low (10 is plenty) if you want to be sure. Start-up is a few seconds plus the three cache commands and the migration check, well inside the roughly one-minute wake a free container takes after sleeping. Composer runs at build time, so a deploy is a few minutes and a wake is not.

Deploying

  1. Add the Dockerfile and a .dockerignore with vendor, node_modules, .env and storage/logs.
  2. If the front end is built with Vite, build it in a first stage (node:22-alpine, npm ci && npm run build) and copy public/build into the PHP image.
  3. Push to GitHub. Containers → Deploy from GitHub → pick the repository, add the variables from the table, attach or paste a database.
  4. Deploy, then open /up (Laravel 11+ ships a health route) on the container URL.

Test the image locally first

Every Laravel deployment problem worth having shows up on your own machine if you run the real image once:

docker build -t myapp .
docker run --rm -p 8080:80 -e APP_KEY=base64:... -e APP_URL=http://localhost:8080 -e DB_CONNECTION=sqlite -e DB_DATABASE=/tmp/test.sqlite myapp
curl -i http://localhost:8080/up

A 500 with a blank page here usually means APP_KEY is missing or storage/ is not writable; a 404 on every route means the document root is not public/. Both are cheaper to find now than after a five-minute build on the platform.

Where free stops for Laravel

  • Free containers sleep after 15 minutes without traffic and take about a minute to wake; the first visitor sees a wake page, then the app.
  • 100 container-hours a month across four containers, counted while awake. A Laravel app plus a sync queue lives comfortably inside that; a worker container does not, because it sleeps.
  • No free database. MySQL or PostgreSQL Mini is $29/month, or use Neon's free Postgres and accept its idle suspend.
  • Uploads and generated files must go to S3 or a compatible bucket; the container's disk is reset on every deploy and every wake.
  • Custom domains need Always-On ($12/month), which also removes the sleeping and makes a queue worker viable.

Free tier: 4 containers, 512 MB and 0.25 vCPU each, 100 container-hours a month, no card, sleeps after 15 minutes. MySQL, MariaDB and PostgreSQL Mini $29/month, Redis Mini $12/month, or a $1 DB Sprint Pack for 12 hours. Prices on the add-on pricing page.

Ready to Deploy?

Deploy free. 10 deploys a day, 100 hours a month, no credit card.

Run this yourself: Get a dedicated NVIDIA T4 (16 GB) for a flat $499/mo or A10G (24 GB) for $999/mo — never shared, never spot, no hourly metering. See dedicated GPU hosting →

One-click Ollama, vLLM, ComfyUI, Whisper, PyTorch & more — or deploy your own GitHub repo or Docker image. Compare plans.

FIRST OF ITS KIND

Sprint Pack — ₹99 for 24 hours

The first managed container PaaS to offer a one-time 24-hour Always-On pass under $5. Unlimited deploys + Always-On for one Small (512 MB) container — including WebSockets & real-time apps. No subscription, no auto-renewal. Stack two for 48 hours.

Need a managed database too? DB Sprint Pack — ₹99 / 12h for Postgres, MySQL, MariaDB, Mongo, Redis, or RabbitMQ.

Tip: Need 24 hours of Always-On for a demo, weekend, or quick test — or WebSockets & real-time apps? Sprint Pack is ₹99 one-time — no subscription, no auto-renewal.

Need a managed Postgres / MySQL / Mongo / Redis instead? DB Sprint Pack — ₹99 / 12h.

Take SnapDeploy with you

Deploy, monitor and wake your containers from your iPhone.

Download on the App Store

Get DevOps Tips & Updates

Container deployment guides, platform updates, and DevOps best practices. No spam.

Unsubscribe anytime. We respect your privacy.

More Articles