Part of the "Under the hood" series of posts, detailing how stackselect.tech is built, and giving you a better idea how some tools listed on stackselect.tech work.
Firstly, why Render? For stackselect.tech, I was looking for a low-maintenance and affordable hosting solution. I'm okay with setting up servers, maintaining infrastructure-as-code playbooks with ansible but since this is a side-project, I was looking for something capable of taking away all that effort from me.
Render hits that sweet-spot for now where I can create a simple Docker image and deploy changes with a simple
git push. Render builds the docker image and deploys the site with zero downtime. I don't have to log in to any servers. Ever.
The docker image
At the time of writing, this is the
Dockerfile that powers stackselect.tech.
FROM php:7.4-fpm-alpine as composer_deps COPY --from=composer:1.10 /usr/bin/composer /usr/bin/composer WORKDIR /app COPY src/Kernel.php /app/src/Kernel.php COPY composer.json /app/ COPY composer.lock /app/ COPY symfony.lock /app/ RUN composer install --no-dev --optimize-autoloader --no-interaction --no-scripts FROM node:12.16-alpine3.11 as node_deps WORKDIR /app COPY package.json /app/package.json COPY yarn.lock /app/yarn.lock RUN yarn FROM node_deps as frontend_build WORKDIR /app COPY webpack.config.js /app/webpack.config.js COPY postcss.config.js /app/postcss.config.js COPY assets /app/assets RUN yarn encore production FROM php:7.4-fpm-alpine WORKDIR /app RUN \ apk add --no-cache \ nginx=1.16.1-r6 \ bash \ && \ wget https://raw.githubusercontent.com/chrismytton/shoreman/380e745d1c2cd7bc163a1485ee57b20c76395198/shoreman.sh && chmod +x shoreman.sh && mv shoreman.sh /usr/local/bin/shoreman COPY --from=composer_deps /app/vendor /app/vendor COPY --from=frontend_build /app/public/build /app/public/build COPY . /app RUN cp /app/infrastructure/php-fpm/php-fpm.conf /usr/local/etc/php-fpm.conf && \ cp /app/infrastructure/php-fpm/www.conf /usr/local/etc/php-fpm.d/www.conf && \ cp /app/infrastructure/nginx/nginx.conf /etc/nginx/nginx.conf && \ cp /app/infrastructure/nginx/vhost.conf /etc/nginx/conf.d/default.conf RUN ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log COPY Procfile /app/Procfile EXPOSE 8080 RUN addgroup app && adduser -D -G app app && \ chown -R app:app \ /app \ /var/lib/nginx/ \ /etc/nginx USER app ENV APP_ENV prod CMD ["/usr/local/bin/shoreman"]
As you can see, it's a Symfony app, so the first stage of this multistage dockerfile is to install the composer dependencies.
Finally, we create the stage which will ultimately serve our traffic. It's based off the official PHP docker image, to which we add nginx, configure various settings (saved in config files like
nginx.conf). We copy in
shoreman since, in this image, we're running more than one process.
There's arguments that running multiple processes in this way is not ideal but in this scenario it allows us to have a very simple setup with the fewest moving parts.
Deploying this docker image to Render's container-based PaaS
To replicate our setup, after creating an account with Render, you need to select "New Web Service". Connect your repository and configure some production settings. Make sure to select the "Docker" service type and add any production environment variables you need.
If they're secrets, consider using Symfony's secret encryption method to prevent accidentally exposing any credentials.
You'll see logs as Render tries to build and deploy your image.
May 3 02:36:47 PM ==> Cloning from https://github.com/vendor/project... May 3 02:36:53 PM ==> Checking out commit a4da30b4ad18645fd068e10ae0f17f4a1cb8e719 in branch master May 3 02:36:56 PM INFO Downloading base image composer:1.10 May 3 02:37:05 PM INFO Downloading base image php:7.4-fpm-alpine ... May 3 02:38:59 PM ______________________________ May 3 02:38:59 PM < Pushing image to registry... > May 3 02:38:59 PM ------------------------------ May 3 02:38:59 PM \ ^__^ May 3 02:38:59 PM \ (oo)\_______ May 3 02:38:59 PM (__)\ )\/\ May 3 02:38:59 PM ||----w | May 3 02:38:59 PM || || May 3 02:39:05 PM ______ May 3 02:39:05 PM < Done > May 3 02:39:05 PM ------ May 3 02:39:05 PM \ ^__^ May 3 02:39:05 PM \ (oo)\_______ May 3 02:39:05 PM (__)\ )\/\ May 3 02:39:05 PM ||----w | May 3 02:39:05 PM || ||
Additionally, you can go further and add Docker healthchecks to help Render restart your services should something go wrong. You can also set up databases as private-services, but for now, for stackselect, everything runs off an SQLite database that's actually committed to the project along with the rest of the code.
Since your app is available on a subdomain, like
your-project.onrender.com you'll probably also want to set up a custom domain. The setup is a
CNAME record pointing your domain to their servers, and the instructions are all fairly straightforward.
One final benefit is the pull request environments. If I want to test a change, I can create a pull request in my repository on Github. Render automatically detects that pull request and creates an environment with the new changes, pings you a link as a github comment and throws away the environment when it's merged or closed, so there's no need for a staging server at all.
Overall, Render seems like a good option, especially if your project is not too complex. And for $7 a month to try it out, you can try it for yourself in a few hours one evening.