From GitLab to Heroku with Docker
Published: 2020-04-27 16:57:41

After writing the code of the project and testing it in a local development environment, you must create a GitLab repository and upload the code and configure to deploy to Heroku.

## .gitignore
Add a `.gitignore` file to your repository so the temporary files and directories generated during the building process don't be uploaded to GitLab. You can use the templates provided by GitLab or generate this file at [gitignore.io](https://gitignore.io).

![gitignore.io](https://dev-to-uploads.s3.amazonaws.com/i/06ibnlg6j3idogjzmkmw.png)

Just type in the search bar the technologies you're using for the project and it will generate the file for you.

The file will look as follows:

```
# Created by https://www.gitignore.io/api/rust,python
# Edit at https://www.gitignore.io/?templates=rust,python

### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# pyenv
.python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# Mr Developer
.mr.developer.cfg
.project
.pydevproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

### Rust ###
# Generated by Cargo
# will have compiled files and executables
/target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# End of https://www.gitignore.io/api/rust,python
```

## Heroku
Before configuring the repository, you should create a Heroku app.
![A new Heroku app](https://dev-to-uploads.s3.amazonaws.com/i/9mq1wxt3v4yx0kls6gcb.png)

Go to [dashboard.heroku.com/account](https://dashboard.heroku.com/account) and copy the API Key.

## Dockerfile
Create a new Dockerfile in your repository. it will have the following content:

```docker
FROM docker.io/username/rust-python:latest

WORKDIR /home/admin
USER admin
RUN mkdir app
WORKDIR app

RUN env PYTHON_CONFIGURE_OPTS='--enable-shared' pyenv install 3.7.7
ENV LD_LIBRARY_PATH $HOME/.pyenv/versions/3.7.7/lib/

RUN pyenv global 3.7.7
RUN pyenv rehash

RUN rustup override set nightly

COPY pyproject.toml ./

RUN poetry config virtualenvs.create false \
    && poetry lock \
    && poetry export --without-hashes -f requirements.txt --dev \
    |  poetry run pip install -r /dev/stdin \
    && poetry debug

COPY  . ./

RUN poetry install --no-interaction \
    && cargo build --release

CMD ["sh", "run"]
```

According to [this issue](https://github.com/containers/buildah/issues/1981), the container registry address (docker.io) must be specified before the name of the base image, for buildah to pull the image correctly from Docker Hub.

The base image used for the project will be the one created before and available from `username/rust-python:latest`.

Then set the `WORKDIR` to `/home/admin` and the `USER` to `admin`. Create the `app` directory and set it as `WORKDIR`.

Install Python 3.7.7 with shared libraries and set the `LD_LIBRARY_PATH` environment variable.

Specify the version of Python and Rust to be used.

Copy the `pyproject.toml` file. Poetry is being used to manage the dependencies of Python but no virtual environment will be created.

Then lock the dependencies and export to a `requirements.txt` file and install them using `pip`, this is done to generate a cache of them.

Then install Python and Rust dependencies of the project, and build the project for production.

When the Docker image being created is run, the **bash** script named `run` is executed.

The `run` bash script has the following content, so that Heroku knows how to run the app:

```bash
ROCKET_PORT=$PORT ROCKET_ENV=prod ./target/release/rust-python-demo
```

The variable `$PORT` is set by Heroku as it dynamically chooses a port for the app, this value is assigned to `ROCKET_PORT`. Then the environment is set to production with `ROCKET_ENV=prod` and the binary of the app is run.

## GitLab CI
Add the value of the API Key of Heroku that you copied before to the settings of the repository by following the steps bellow:

- Go to `Settings` > `CI/CD`.
- In the `Variables` section click on `Expand`.
- Add the variable `HEROKU_API_KEY` and assign the value copied before.

![Heroku API Key](https://dev-to-uploads.s3.amazonaws.com/i/uj3omnm488onrtoni4ug.png)

Then add the `.gitlab-ci.yml` file, this is the configuration file of the Continuous Integration system provided by GitLab.

For this I just follow Alessandro Diaferia's tutorial on [Continuous Deployment With GitLab, Docker And Heroku](https://alediaferia.com/2019/12/07/continuous-deployment-gitlab-docker-heroku/).

The file will look as follows:

```yaml
stages:
  - build
  - release

build_image:
  only:
    - master
  image: registry.gitlab.com/majorhayden/container-buildah
  stage: build
  variables:
    STORAGE_DRIVER: "vfs"
    BUILDAH_FORMAT: "docker"
  before_script:
    - dnf install -y nodejs
    - curl https://cli-assets.heroku.com/install.sh | sh
    - sed -i '/^mountopt =.*/d' /etc/containers/storage.conf
  script:
    - buildah bud --iidfile iidfile -t rust-python-demo:$CI_COMMIT_SHORT_SHA .
    - buildah push --creds=_:$(heroku auth:token) $(cat iidfile) registry.heroku.com/heroku-app-name /web

release:
  only:
    - master
  image: node:10.17-alpine
  stage: release
  before_script:
    - apk add curl bash
    - curl https://cli-assets.heroku.com/install.sh | sh
  script:
    - heroku container:release -a heroku-app-name web
```

It will only have two stages, `build_image`, where the Docker image is built using the Dockerfile created previously and added to the registry of Heroku, and `release`, where the image will be pushed to the Heroku app.

Just replace `registry.heroku.com/heroku-app-name/web` and `heroku container:release -a heroku-app-name web` with the name of the app you created on Heroku.

If everything goes well, the jobs of the pipeline will succeed and your app is deployed to Heroku.
![GitLab Pipeline](https://dev-to-uploads.s3.amazonaws.com/i/ft3fmsaju1qky2rbixx4.png)

You can go to [rust-python-demo.herokuapp.com](https://rust-python-demo.herokuapp.com) to see a live demo running.
![Heroku App running on Firefox](https://dev-to-uploads.s3.amazonaws.com/i/6egxoyeitujdv0140s1q.png)