<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Some writings about software]]></title><description><![CDATA[Thoughts, stories and ideas.]]></description><link>https://nathanjaremko.com/</link><image><url>https://nathanjaremko.com/favicon.png</url><title>Some writings about software</title><link>https://nathanjaremko.com/</link></image><generator>Ghost 5.2</generator><lastBuildDate>Wed, 27 Aug 2025 04:58:08 GMT</lastBuildDate><atom:link href="https://nathanjaremko.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Deploying an IHP project to Fly.io]]></title><description><![CDATA[<p>Learning Haskell the conventional way can be difficult, so I&#x2019;m going to recommend a different path, you can dive in head-first with the &#x201C;Ruby on Rails&#x201D; of Haskell: IHP.</p><p><a href="https://www.haskell.org/" rel="nofollow ugc noopener">Haskell</a> is awesome. If you haven&#x2019;t used it, you should give it a shot. There</p>]]></description><link>https://nathanjaremko.com/coming-soon/</link><guid isPermaLink="false">6442f1f220384c02104fcc84</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Nathan Jaremko]]></dc:creator><pubDate>Fri, 21 Apr 2023 20:28:34 GMT</pubDate><content:encoded><![CDATA[<p>Learning Haskell the conventional way can be difficult, so I&#x2019;m going to recommend a different path, you can dive in head-first with the &#x201C;Ruby on Rails&#x201D; of Haskell: IHP.</p><p><a href="https://www.haskell.org/" rel="nofollow ugc noopener">Haskell</a> is awesome. If you haven&#x2019;t used it, you should give it a shot. There are a few paths to learning Haskell. You could read <a href="https://haskellbook.com/" rel="nofollow ugc noopener">the book</a>, but that&#x2019;s &gt;1000 pages of knowledge that will probably scare you off if you&#x2019;re just getting into it. So I&#x2019;m going to recommend a different path, you can dive in head-first with the &#x201C;Ruby on Rails&#x201D; of Haskell: <a href="https://ihp.digitallyinduced.com/" rel="nofollow ugc noopener">IHP</a>.</p><p>After you&#x2019;ve got everything working, and you&#x2019;ve made your first Haskell web application. You&#x2019;ll probably be thinking to yourself &#x201C;they&#x2019;ve been talking up nix so much, so how do I deploy this?&#x201D;. I&#x2019;ve got good news. It&#x2019;s actually very easy to create an optimized, many-layered, reproducible docker image with nix, and I&#x2019;m going to show you how. Then we&#x2019;re going to:</p><ul><li>Deploy it for free on <a href="https://fly.io/" rel="nofollow ugc noopener">fly.io</a></li><li>Setup CI/CD to deploy every commit automatically with github actions.</li></ul><h2 id="making-an-ihp-docker-image">Making an IHP docker image</h2><p>First things first, you&#x2019;re going to follow the <a href="https://ihp.digitallyinduced.com/Guide/installation.html" rel="nofollow ugc noopener">IHP setup instructions</a>. Come back when you&#x2019;ve got everything working.</p><p>Welcome back!</p><p>At the time of me writing this, in your newly created IHP project, you&#x2019;ll find a <code>default.nix</code> file with the following contents:</p><pre><code>let
    ihp = builtins.fetchGit {
        url = &quot;https://github.com/digitallyinduced/ihp.git&quot;;
        ref = &quot;refs/tags/v0.19.0&quot;;
    };
    haskellEnv = import &quot;${ihp}/NixSupport/default.nix&quot; {
        ihp = ihp;
        haskellDeps = p: with p; [
            cabal-install
            base
            wai
            text
            hlint
            p.ihp
        ];
        otherDeps = p: with p; [
            # Native dependencies, e.g. imagemagick
        ];
        projectPath = ./.;
    };
in
    haskellEnv</code></pre><p>With that, if you were to run the following command in the project directory:</p><pre><code>nix-build &#x2014;option sandbox false</code></pre><p>you&#x2019;d end up with a <code>result/bin</code> folder in your current directory containing a binary to run your IHP project. You might be thinking &#x201C;Cool! Now how do I ship this on Fly.io?&#x201D;</p><p>We&#x2019;ll get there, but first I&#x2019;m going to explain what each of these blocks are doing:</p><pre><code>ihp = builtins.fetchGit {
    url = &quot;https://github.com/digitallyinduced/ihp.git&quot;;
    ref = &quot;refs/tags/v0.19.0&quot;;
};</code></pre><p>This block downloads a copy of the IHP git repo, specifically the 0.19.0 tag and stores it in the <code>ihp</code> variable.</p><pre><code>haskellEnv = import &quot;${ihp}/NixSupport/default.nix&quot; {
    ihp = ihp;
    haskellDeps = p: with p; [
        cabal-install
        base
        wai
        text
        hlint
        p.ihp
    ];
    otherDeps = p: with p; [
        # Native dependencies, e.g. imagemagick
    ]; 
    projectPath = ./.;
};</code></pre><p>This block imports a nix function from the IHP git repo we just fetched, and passes some named arguments to the function. <code>ihp</code> is just passing in the repo, <code>haskellDeps</code> is a list of haskell dependencies that we need to build the project, <code>otherDeps</code> is a list of native dependencies we need to build the project, and <code>projectPath</code> is the path to copy all your files from.</p><p>To facilitate making our docker image, we need to make a new file <code>app.nix</code> that defines a function that returns a similar value to our current <code>default.nix</code></p><pre><code># app.nix

{ additionalNixpkgsOptions ? {}, optimized ? false }:

let
    ihp = builtins.fetchGit {
        url = &quot;https://github.com/digitallyinduced/ihp.git&quot;;
        ref = &quot;refs/tags/v0.19.0&quot;;
    };
    haskellEnv = import &quot;${ihp}/NixSupport/default.nix&quot; {
        ihp = ihp;
        haskellDeps = p: with p; [
            cabal-install
            base
            wai
            text
            hlint
            p.ihp
        ];
        otherDeps = p: with p; [
            # Native dependencies, e.g. imagemagick
        ];
        additionalNixpkgsOptions = additionalNixpkgsOptions; 
        projectPath = ./.;
        optimized = optimized;
    };
in
    haskellEnv</code></pre><p>With that file created, we can update our <code>default.nix</code> file like so:</p><pre><code class="language-nix"># default.nix

let
    haskellEnv = import ./app.nix {};
in
    haskellEnv</code></pre><p>Now, for the good part, let&#x2019;s make a docker image. Create a new file <code>docker.nix</code> like so:</p><pre><code class="language-nix"># docker.nix

{ localPkgs ? import &lt;nixpkgs&gt; {}
, imagePkgs ? import &lt;nixpkgs&gt; { system = &quot;x86_64-linux&quot;; }
, ihpApp ? import ./app.nix { 
    additionalNixpkgsOptions = { system = &quot;x86_64-linux&quot;; }; 
    optimized = true; 
  }
}:

localPkgs.dockerTools.buildLayeredImage {
  name = &quot;app&quot;;
  contents = [ imagePkgs.cacert imagePkgs.iana-etc imagePkgs.openssl ];
  config = {
    Cmd = [ &quot;${ihpApp}/bin/RunProdServer&quot; ];
    WorkingDir = &quot;${ihpApp}/lib&quot;;
    ExposedPorts = {
      &quot;8000&quot; = {};
    };
  };
  maxLayers = 128;
}</code></pre><p>You can now, if you&#x2019;re running linux, you can run the following command to create an optimized linux build of the IHP project and load it into docker:</p><pre><code>docker load &lt; $(nix-build ./docker.nix --option sandbox false)</code></pre><p>If you have the misfortune of using Windows or MacOS, you&#x2019;ll need to use the following <code>Dockerfile</code> to build the image:</p><pre><code># This file lets you build the image on windows/mac
# Run `docker build . -t app`
# Then run `docker run -d -t app` to run the image
# Then run `docker ps` to get the hash of the running container
# Then use that and run `docker cp -L TheContainerHash:/app/result ./app.tar.gz`
# Then run `docker load &lt; ./app.tar.gz`

FROM nixos/nix

RUN nix-channel --update

WORKDIR /app

RUN nix-env -iA cachix -f https://cachix.org/api/v1/install
RUN cachix use digitallyinduced

ADD . .

RUN nix-build ./docker.nix --option sandbox false</code></pre><h2 id="deploying-on-flyio">Deploying on Fly.io</h2><p>Now we&#x2019;ll get started with deploying to fly.io!</p><ol><li>Go follow the instructions on their website to get their CLI setup.</li><li>Run <code>flyctl apps create</code> and create an app named &#x201C;app&#x201D;</li><li>Follow <a href="https://fly.io/docs/reference/postgres/" rel="nofollow ugc noopener">the instructions</a> to create and attach a postgres instance to your app</li></ol><p>Make a <code>fly.toml</code> file in your project like so:</p><pre><code># fly.toml file generated for app on 2022-06-10T20:32:18-07:00

app = &quot;app&quot;

kill_signal = &quot;SIGINT&quot;
kill_timeout = 5
processes = []

[build]
  image = &quot;app&quot;

[env]
  PORT = &quot;8000&quot;
  IHP_REQUEST_LOGGER_IP_ADDR_SOURCE = &quot;FromHeader&quot;
  IHP_BASEURL = &quot;https://YOURURLHERE.com&quot;

[experimental]
  allowed_public_ports = []
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 8000
  processes = [&quot;app&quot;]
  protocol = &quot;tcp&quot;
  script_checks = []

  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = &quot;connections&quot;

  [[services.ports]]
    force_https = true
    handlers = [&quot;http&quot;]
    port = 80

  [[services.ports]]
    handlers = [&quot;tls&quot;, &quot;http&quot;]
    port = 443

  [[services.tcp_checks]]
    grace_period = &quot;1s&quot;
    interval = &quot;15s&quot;
    restart_limit = 0
    timeout = &quot;2s&quot;
</code></pre><p>You&#x2019;ll need to create a secret <code>IHP_SESSION_SECRET</code> , I used <code>pwgen -s 128 1</code> to generate one:</p><pre><code>flyctl secrets set IHP_SESSION_SECRET=$(pwgen -s 128 1)</code></pre><p>Now find the image in your local docker</p><pre><code>docker image ls</code></pre><p>and finally run</p><pre><code>flyctl deploy -i app:theTagFromThePreviousCommand</code></pre><h2 id="deploying-automatically-with-github-actions">Deploying automatically with Github Actions</h2><p>If you want to have it automatically deployed to fly every commit in your repo, you&#x2019;ll need to go <a href="https://fly.io/user/personal_access_tokens" rel="nofollow ugc noopener">create an API token</a> and add it as a secret in your repo settings, then you can create <code>.github/workflows/deploy.yml</code> like so</p><pre><code>name: &quot;Build Image&quot;
on:
  pull_request:
  push:
env:
  FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
  # This should be of the form postgresql://username:password@localhost/db_name
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2.4.0
      - uses: cachix/install-nix-action@v17
        with:
          nix_path: nixpkgs=channel:nixos-22.05
      - uses: cachix/cachix-action@v10
        with:
          name: digitallyinduced
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: docker load &lt; $(nix-build ./docker.nix --option sandbox false)
      - run: flyctl proxy 5432 -a app-db &amp;
      # Give 10 seconds for the proxy to start up, 5 seconds would probably work too
      - run: sleep 10 &amp;&amp; nix-shell --run migrate
      - run: flyctl deploy -i app:$(docker image ls &quot;app:*&quot; -a --format &apos;table {{.Tag}}&apos; | sed -sn 2p)</code></pre>]]></content:encoded></item></channel></rss>